/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smack.sasl;

import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.StringUtils;

import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
import org.apache.harmony.javax.security.auth.callback.Callback;
import org.apache.harmony.javax.security.auth.callback.NameCallback;
import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
import org.apache.harmony.javax.security.sasl.RealmCallback;
import org.apache.harmony.javax.security.sasl.RealmChoiceCallback;
import de.measite.smack.Sasl;
import org.apache.harmony.javax.security.sasl.SaslClient;
import org.apache.harmony.javax.security.sasl.SaslException;

/**
 * Base class for SASL mechanisms. Subclasses must implement these methods:
 * <ul>
 * <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li>
 * </ul>
 * Subclasses will likely want to implement their own versions of these mthods:
 * <li>{@link #authenticate(String, String, String)} -- Initiate authentication
 * stanza using the deprecated method.</li> <li>
 * {@link #authenticate(String, String, CallbackHandler)} -- Initiate
 * authentication stanza using the CallbackHandler method.</li> <li>
 * {@link #challengeReceived(String)} -- Handle a challenge from the server.</li>
 * </ul>
 * 
 * @author Jay Kline
 */
public abstract class SASLMechanism implements CallbackHandler {

	private SASLAuthentication saslAuthentication;
	protected SaslClient sc;
	protected String authenticationId;
	protected String password;
	protected String hostname;

	public SASLMechanism(SASLAuthentication saslAuthentication) {
		this.saslAuthentication = saslAuthentication;
	}

	/**
	 * Builds and sends the <tt>auth</tt> stanza to the server. Note that this
	 * method of authentication is not recommended, since it is very inflexable.
	 * Use {@link #authenticate(String, String, CallbackHandler)} whenever
	 * possible.
	 * 
	 * @param username
	 *            the username of the user being authenticated.
	 * @param host
	 *            the hostname where the user account resides.
	 * @param password
	 *            the password for this account.
	 * @throws IOException
	 *             If a network error occurs while authenticating.
	 * @throws XMPPException
	 *             If a protocol error occurs or the user is not authenticated.
	 */
	public void authenticate(String username, String host, String password)
			throws IOException, XMPPException {
		// Since we were not provided with a CallbackHandler, we will use our
		// own with the given
		// information

		// Set the authenticationID as the username, since they must be the same
		// in this case.
		this.authenticationId = username;
		this.password = password;
		this.hostname = host;

		String[] mechanisms = { getName() };
		Map<String, String> props = new HashMap<String, String>();
		sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props,
				this);
		authenticate();
	}

	/**
	 * Builds and sends the <tt>auth</tt> stanza to the server. The callback
	 * handler will handle any additional information, such as the
	 * authentication ID or realm, if it is needed.
	 * 
	 * @param username
	 *            the username of the user being authenticated.
	 * @param host
	 *            the hostname where the user account resides.
	 * @param cbh
	 *            the CallbackHandler to obtain user information.
	 * @throws IOException
	 *             If a network error occures while authenticating.
	 * @throws XMPPException
	 *             If a protocol error occurs or the user is not authenticated.
	 */
	public void authenticate(String username, String host, CallbackHandler cbh)
			throws IOException, XMPPException {
		String[] mechanisms = { getName() };
		Map<String, String> props = new HashMap<String, String>();
		sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props,
				cbh);
		authenticate();
	}

	protected void authenticate() throws IOException, XMPPException {
		String authenticationText = null;
		try {
			if (sc.hasInitialResponse()) {
				byte[] response = sc.evaluateChallenge(new byte[0]);
				authenticationText = StringUtils.encodeBase64(response, false);
			}
		} catch (SaslException e) {
			throw new XMPPException("SASL authentication failed", e);
		}

		// Send the authentication to the server
		getSASLAuthentication().send(
				new AuthMechanism(getName(), authenticationText));
	}

	/**
	 * The server is challenging the SASL mechanism for the stanza he just sent.
	 * Send a response to the server's challenge.
	 * 
	 * @param challenge
	 *            a base64 encoded string representing the challenge.
	 * @throws IOException
	 *             if an exception sending the response occurs.
	 */
	public void challengeReceived(String challenge) throws IOException {
		byte response[];
		if (challenge != null) {
			response = sc
					.evaluateChallenge(StringUtils.decodeBase64(challenge));
		} else {
			response = sc.evaluateChallenge(new byte[0]);
		}

		Packet responseStanza;
		if (response == null) {
			responseStanza = new Response();
		} else {
			responseStanza = new Response(StringUtils.encodeBase64(response,
					false));
		}

		// Send the authentication to the server
		getSASLAuthentication().send(responseStanza);
	}

	/**
	 * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or
	 * GSSAPI.
	 * 
	 * @return the common name of the SASL mechanism.
	 */
	protected abstract String getName();

	protected SASLAuthentication getSASLAuthentication() {
		return saslAuthentication;
	}

	/**
     * 
     */
	public void handle(Callback[] callbacks) throws IOException,
			UnsupportedCallbackException {
		for (int i = 0; i < callbacks.length; i++) {
			if (callbacks[i] instanceof NameCallback) {
				NameCallback ncb = (NameCallback) callbacks[i];
				ncb.setName(authenticationId);
			} else if (callbacks[i] instanceof PasswordCallback) {
				PasswordCallback pcb = (PasswordCallback) callbacks[i];
				pcb.setPassword(password.toCharArray());
			} else if (callbacks[i] instanceof RealmCallback) {
				RealmCallback rcb = (RealmCallback) callbacks[i];
				rcb.setText(hostname);
			} else if (callbacks[i] instanceof RealmChoiceCallback) {
				// unused
				// RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i];
			} else {
				throw new UnsupportedCallbackException(callbacks[i]);
			}
		}
	}

	/**
	 * Initiating SASL authentication by select a mechanism.
	 */
	public class AuthMechanism extends Packet {
		final private String name;
		final private String authenticationText;

		public AuthMechanism(String name, String authenticationText) {
			if (name == null) {
				throw new NullPointerException(
						"SASL mechanism name shouldn't be null.");
			}
			this.name = name;
			this.authenticationText = authenticationText;
		}

		public String toXML() {
			StringBuilder stanza = new StringBuilder();
			stanza.append("<auth mechanism=\"").append(name);
			stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
			if (authenticationText != null
					&& authenticationText.trim().length() > 0) {
				stanza.append(authenticationText);
			}
			stanza.append("</auth>");
			return stanza.toString();
		}
	}

	/**
	 * A SASL challenge stanza.
	 */
	public static class Challenge extends Packet {
		final private String data;

		public Challenge(String data) {
			this.data = data;
		}

		public String toXML() {
			StringBuilder stanza = new StringBuilder();
			stanza.append("<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
			if (data != null && data.trim().length() > 0) {
				stanza.append(data);
			}
			stanza.append("</challenge>");
			return stanza.toString();
		}
	}

	/**
	 * A SASL response stanza.
	 */
	public class Response extends Packet {
		final private String authenticationText;

		public Response() {
			authenticationText = null;
		}

		public Response(String authenticationText) {
			if (authenticationText == null
					|| authenticationText.trim().length() == 0) {
				this.authenticationText = null;
			} else {
				this.authenticationText = authenticationText;
			}
		}

		public String toXML() {
			StringBuilder stanza = new StringBuilder();
			stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
			if (authenticationText != null) {
				stanza.append(authenticationText);
			}
			stanza.append("</response>");
			return stanza.toString();
		}
	}

	/**
	 * A SASL success stanza.
	 */
	public static class Success extends Packet {
		final private String data;

		public Success(String data) {
			this.data = data;
		}

		public String toXML() {
			StringBuilder stanza = new StringBuilder();
			stanza.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
			if (data != null && data.trim().length() > 0) {
				stanza.append(data);
			}
			stanza.append("</success>");
			return stanza.toString();
		}
	}

	/**
	 * A SASL failure stanza.
	 */
	public static class Failure extends Packet {
		final private String condition;

		public Failure(String condition) {
			this.condition = condition;
		}

		/**
		 * Get the SASL related error condition.
		 * 
		 * @return the SASL related error condition.
		 */
		public String getCondition() {
			return condition;
		}

		public String toXML() {
			StringBuilder stanza = new StringBuilder();
			stanza.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
			if (condition != null && condition.trim().length() > 0) {
				stanza.append("<").append(condition).append("/>");
			}
			stanza.append("</failure>");
			return stanza.toString();
		}
	}
}
